
初学者对TableGen语言感到不知所措，开始尝试这门语言后，就会发现没那么难。

\mySubsubsection{8.3.1.}{定义记录和类}

让我们为指令定义一条简单的记录:

\begin{shell}
def ADD {
    string Mnemonic = "add";
    int Opcode = 0xA0;
}
\end{shell}

def关键字表示定义了一条记录，后面跟着记录的名称。记录主体由花括号包围，主体由字段定义组成，类似于C++中的结构体。

可以使用llvm-tblgen工具查看生成的记录，将上述源代码保存在inst.td文件中，然后运行如下命令:

\begin{shell}
$ llvm-tblgen --print-records inst.td
------------- Classes -----------------
------------- Defs -----------------
def ADD {
    string Mnemonic = "add";
    int Opcode = 160;
}
\end{shell}

这只表明正确的解析了定义的记录。

定义指令使用单一的记录不是很舒服。现代的CPU有数百条指令，有了这么多的记录，很容易在字段名中引入输入错误。若决定重命名一个字段或添加一个新字段，则要更改的记录数量将会非常的大，所以蓝图必不可少。C++中，类也有类似的用途；TableGen中，也被称为类。下面是一个Inst类的定义，以及基于该类的两条记录:

\begin{shell}
class Inst<string mnemonic, int opcode> {
    string Mnemonic = mnemonic;
    int Opcode = opcode;
}

def ADD : Inst<"add", 0xA0>;
def SUB : Inst<"sub", 0xB0>;
\end{shell}

类的语法类似于记录的语法。class关键字表示定义了一个类，后跟类名。类可以有一个参数列表，Inst类有两个参数，助记符和操作码，用于初始化记录的字段。这些字段的值在类实例化时给出。ADD和SUB记录显示了类的两个实例化，先使用llvm-tblgen来查看记录:

\begin{shell}
$ llvm-tblgen --print-records inst.td
------------- Classes -----------------
class Inst<string Inst:mnemonic = ?, int Inst:opcode = ?> {
    string Mnemonic = Inst:mnemonic;
    int Opcode = Inst:opcode;
}
------------- Defs -----------------
def ADD { // Inst
    string Mnemonic = "add";
    int Opcode = 160;
}
def SUB { // Inst
    string Mnemonic = "sub";
    int Opcode = 176;
}
\end{shell}

现在，有了一个类定义和两条记录，用于定义记录的类的名称显示为注释。注意，该类的参数具有默认值“?”，这表示int未初始化。

\begin{myTip}{调试技巧}
要获得更详细的记录转储，可以使用-{}-print-detailed-records选项。输出包括记录和类定义的行号，以及初始化记录字段的位置。可用来跟踪一个记录字段如何分配了某个值。
\end{myTip}

ADD和SUB指令有很多共同之处，但加法是可交换运算，而减法不是。这里的一个小小的挑战是，TableGen只支持有限的数据类型集。已经在示例中使用了string和int，其他可用的数据类型有bit、bits<n>、list<type>和dag。位类型表示单个位;也就是0或1。若需要固定数量的位，则使用bits<n>类型。例如，bits<5>是一个宽为5位的整数类型。基于另一种类型定义列表，可以使用list<type>类型。例如，list<int>是一个整数列表，而list<Inst>是示例中Inst类的记录列表。dag类型表示有向无环图(dag)节点，对于定义模式和操作很有用，并且在LLVM后端中广泛使用。

使用单个比特表示一个标志就足够了，因此可以使用一个比特将指令标记为可交换的。大多数指令都不可交换，所以可以使用默认值:

\begin{shell}
class Inst<string mnemonic, int opcode, bit commutable = 0> {
    string Mnemonic = mnemonic;
    int Opcode = opcode;
    bit Commutable = commutable;
}

def ADD : Inst<"add", 0xA0, 1>;
def SUB : Inst<"sub", 0xB0>;
\end{shell}

应该运行llvm-tblgen来验证记录是否按预期定义。

类不需要有参数，也可以稍后赋值。可以定义所有指令都不可交换:

\begin{shell}
class Inst<string mnemonic, int opcode> {
    string Mnemonic = mnemonic;
    int Opcode = opcode;
    bit Commutable = 0;
}

def SUB : Inst<"sub", 0xB0>;
\end{shell}

使用let语句，可以覆盖该值:

\begin{shell}
let Commutable = 1 in
    def ADD : Inst<"add", 0xA0>;
\end{shell}

或者，打开记录主体来覆盖该值:

\begin{shell}
def ADD : Inst<"add", 0xA0> {
    let Commutable = 1;
}
\end{shell}

同样，请使用llvm-tblgen来验证在这两种情况下，是否将可交换标志设置为1。

类和记录可以从多个类继承，并且可以添加新字段或覆盖现有字段的值。可以使用继承来引入一个新的可交换类:

\begin{shell}
class Inst<string mnemonic, int opcode> {
    string Mnemonic = mnemonic;
    int Opcode = opcode;
    bit Commutable = 0;
}

class CommutableInst<string mnemonic, int opcode>
    : Inst<mnemonic, opcode> {
    let Commutable = 1;
}

def SUB : Inst<"sub", 0xB0>;
def ADD : CommutableInst<"add", 0xA0>;
\end{shell}

生成的记录总相同，但是该语言允许以不同的方式定义记录。注意，在后一个示例中，可交换标志可能是多余的:代码生成器可以查询它所基于的类的记录，若该列表包含可交换类，则可以在内部设置该标志。

\mySubsubsection{8.3.2.}{使用多个类一次创建多个记录}

另一个常用语句是multiclass，multiclass允许一次定义多个记录，让我们扩展这个示例。

add指令的定义非常简单，一个CPU通常有几个add指令。一种常见的变体是一条指令有两个寄存器操作数，而另一条指令有一个寄存器操作数和一个直接操作数，这是一个小数字。假设对于具有直接操作数的指令，指令集的设计者决定用i作为后缀来标记，所以以add和addi指令结束。此外，假设操作码相差1。许多算术和逻辑指令遵循这个方案，所以定义需要尽可能紧凑。

第一个挑战是需要操作值，可用于修改值的操作符数量有限。例如，要生成1和字段操作码的值的和:

\begin{shell}
!add(opcode, 1)
\end{shell}

这样的表达式最好用作类的参数。测试一个字段值，需要不可用的动态语句，所以不可能根据找到的值更改。记住，所有的计算都是在构造记录时完成的!

类似地，字符串也可以连接起来:

\begin{shell}
!strconcat(mnemonic,"i")
\end{shell}

因为所有操作符都以感叹号(!)开头，所以也称为bang操作符。可以在开发者参考:\url{https://llvm.org/docs/TableGen/ProgRef.html#appendix-a-bang-operators}中找到bang操作符的完整列表。

现在，可以定义一个多类。Inst类再次作为基类:

\begin{shell}
class Inst<string mnemonic, int opcode> {
    string Mnemonic = mnemonic;
    int Opcode = opcode;
}
\end{shell}

多类的定义有点复杂，分步骤来做:

\begin{enumerate}
\item
多类的定义使用与类相似的语法。新的多类名为InstWithImm，有两个参数，助记符和操作码:

\begin{shell}
multiclass InstWithImm<string mnemonic, int opcode> {
\end{shell}

\item
首先，定义一条带有两个寄存器操作数的指令。与普通的记录定义一样，使用def关键字定义记录，并使用Inst类创建记录内容。还需要定义一个空名称，将在后面解释为什么这么做:

\begin{shell}
    def "": Inst<mnemonic, opcode>;
\end{shell}

\item
接下来，用直接操作数定义一条指令。可以使用bang操作符从多类的参数中派生助记符和操作码的值，记录命名为I:

\begin{shell}
    def I: Inst<!strconcat(mnemonic,"i"), !add(opcode, 1)>;
\end{shell}

\item
这就是全部了，类主体已经完成了:

\begin{shell}
}
\end{shell}
\end{enumerate}

要实例化记录，必须使用defm关键字:

\begin{shell}
defm ADD : InstWithImm<"add", 0xA0>;
\end{shell}

语句的结果如下所示:

\begin{enumerate}
\item
Inst<"add", 0xA0>记录实例化，记录的名称由multiclass语句中defm关键字后面的名称和def后面的名称拼接而成，因此名称为ADD。

\item
为基于类型的别名分析生成元数据:将向LLVM IR添加的元数据，这将帮助LLVM更好地优化代码

\item
Inst<"addi", 0xA1>记录实例化，并按照相同的模式命名为ADDI。
\end{enumerate}

用llvm-tblgen验证这个声明:

\begin{shell}
$ llvm-tblgen –print-records inst.td
------------- Classes -----------------
class Inst<string Inst:mnemonic = ?, int Inst:opcode = ?> {
    string Mnemonic = Inst:mnemonic;
    int Opcode = Inst:opcode;
}
------------- Defs -----------------
def ADD { // Inst
    string Mnemonic = "add";
    int Opcode = 160;
    }
def ADDI { // Inst
    string Mnemonic = "addi";
    int Opcode = 161;
}
\end{shell}

使用multiclass，可以一次生成多个记录。这个功能会经常使用!

记录不需要有名称，匿名记录完全没问题，省略名称是定义匿名记录所需要做的全部工作。由多类生成的记录的名称由两个名称组成，并且必须给出两个名称才能创建命名记录。若省略def后面的名称，则只创建匿名记录。若多类中的def后面没有名称，则会创建一个匿名记录。这就是多类示例中的第一个定义使用空名称""的原因:没有它，记录是匿名的。

\mySubsubsection{8.3.3.}{模拟函数调用}

某些情况下，使用前面例子中的多分类可能会导致重复。假定CPU也支持内存操作数，类似于直接操作数。可以通过在多类中添加一个新的记录来定义:

\begin{shell}
multiclass InstWithOps<string mnemonic, int opcode> {
    def "": Inst<mnemonic, opcode>;
    def "I": Inst<!strconcat(mnemonic,"i"), !add(opcode, 1)>;
    def "M": Inst<!strconcat(mnemonic,"m"), !add(opcode, 2)>;
}
\end{shell}

这完全没问题，但假设要定义的记录不是3条，而是16条，并且需要多次这样做。出现这种情况的场景是，CPU支持许多向量类型，并且向量指令根据所使用的类型略有不同。

请注意，带有def语句的所有三行都具有相同的结构。变化只存在于名称和助记符的后缀中，增量值会添加到操作码中。C语言中，可以将数据放入一个数组中，并实现一个函数，该函数根据索引值返回数据。然后，可以在数据上创建一个循环，而不是手动重复书写语句。

令人惊讶的是，可以在TableGen语言中做类似的事情!下面是如何转换示例:

\begin{enumerate}
\item
要存储数据，需要定义一个包含所有必需字段的类。这个类称为InstDesc，其描述了指令的一些属性:

\begin{shell}
class InstDesc<string name, string suffix, int delta> {
    string Name = name;
    string Suffix = suffix;
    int Delta = delta;
}
\end{shell}

\item
现在，可以为每个操作数类型定义记录。注意，其准确地捕获了数据中观察到的差异:

\begin{shell}
def RegOp : InstDesc<"", "", 0>;
def ImmOp : InstDesc<"I", """, 1>;
def MemOp : InstDesc"""","""", 2>;
\end{shell}

\item
假设有一个枚举数字0、1和2的循环，并且希望根据索引选择先前定义的记录之一，需要怎么做呢?解决方案是创建一个以索引作为参数的getDesc类。它有一个字段ret，可以将其解释为返回值。要给这个字段赋正确的值，使用第二个操作符:

\begin{shell}
class getDesc<int n> {
    InstDesc ret = !cond(!eq(n, 0) : RegOp,
                         !eq(n, 1) : ImmOp,
                         !eq(n, 2) : MemOp);
}
\end{shell}

此操作符的工作方式类似于C中的switch/case语句。

\item
现在，可以定义多类了。TableGen语言有一个循环语句，允许定义变量，但没有动态执行!因此，循环范围是静态定义的，可以为变量赋值，但不能更改该值，但这也足以检索数据。注意getDesc类的使用类似于函数调用，但是没有函数调用!相反，将创建一个匿名记录，并从该记录中获取值。最后，past操作符(\#)执行字符串连接，类似于前面使用的!strconcat操作符:

\begin{shell}
multiclass InstWithOps<string mnemonic, int opcode> {
    foreach I = 0-2 in {
        defvar Name = getDesc<I>.ret.Name;
        defvar Suffix = getDesc<I>.ret.Suffix;
        defvar Delta = getDesc<I>.ret.Delta;
        def Name: Inst<mnemonic # Suffix,
                            !add(opcode, Delta)>;
    }
}
\end{shell}

\item
现在，可以像以前一样使用multiclass来定义记录:

\begin{shell}
defm ADD : InstWithOps<"add", 0xA0>;
\end{shell}
\end{enumerate}

请运行llvm-tblgen查看记录。除了各种ADD记录之外，还将看到使用getDesc类生成的一些匿名记录。

几个LLVM后端的指令定义中使用了这种技术。根据现在所掌握的知识，阅读这些文件应该没有问题。

foreach语句使用语法0-2来表示范围的边界，这叫做值域。另一种语法是使用三个点(0…3)，这在数字为负数时很有用。最后，不局限于数值范围，还可以遍历元素列表，可以使用字符串或先前定义的记录。例如，可能喜欢使用foreach语句，但觉得使用getDesc类过于复杂。这种情况下，循环InstDesc记录是解决方案:

\begin{shell}
multiclass InstWithOps<string mnemonic, int opcode> {
    foreach I = [RegOp, ImmOp, MemOp] in {
        defvar Name = I.Name;
        defvar Suffix = I.Suffix;
        defvar Delta = I.Delta;
        def Name: Inst<mnemonic # Suffix, !add(opcode, Delta)>;
    }
}
\end{shell}

目前，只使用最常用的语句在TableGen语言中定义了记录。下一节中，将了解如何从TableGen语言中定义的记录生成C++源代码。




